几周以前,我正在漫无目的的浏览Hacker News,读到一篇关于Redux的头条新闻,Redux的内容我是了解,但是另一个谈到的问题javascript fatigue
(JavaScript 疲劳)已经困扰我了,所以我没有太关心,知道读到Redux的几个特征.
- 强化了函数式编程,确保app行为的可预测性
- 允许app的同构,客户端和服务端的大多数逻辑都可以共享
- 时间旅行的debugger?有可能吗?
Redux似乎是React程序state管理的优雅方法,再者谁说的时间旅行不可能?所以我读了文档和一篇非常精彩的教程@teropa:A Comprehensive Guide to Test-First Development with Redux,React,and Immutable(这一篇也是我写作的主要灵感来源).
我喜欢Redux,代码非常优雅,debugger令人疯狂的伟大.我的意思是-看看这个
接下来的教程第一部分希望引导你理解Redux运行的原则.教程的目的仅限于(客户端,没有同构,是比较简单的app)保持教程的简明扼要.如果你想发掘的更深一点,我仅建议你阅读上面提高的那个教程.对比版的Github repo在这里,共享代码贴合教程的步骤.如果你对代码或者教程有任何问题和建议,最好能留下留言.
编辑按:文章已经更新为ES2015版的句法.
APP
为了符合教程的目的,我们将建一个经典的TodoMVC,为了记录需要,需求如下:
- 每一个todo可以激活或者完成
- 可以添加,编辑,删除一个todo
- 可以根据它的status来过滤筛选todos
- 激活的todos的数目显示在底部
- 完成的Todo理解可以删除
Reudux和Immutable:使用函数编程去营救
回到几个月前,我正在开发一个webapp包含仪表板. 随着app的成长,我们注意到越来越多的有害的bugs,藏在代码角落里,很难发现.类似:“如果你要导航到这一页,点击按钮,然后回到主页,喝一杯咖啡,回到这一页然后点击两次,奇怪的事情发生了.”这些bug的来源要么是异步操作(side effects)或者逻辑:一个action可能在app中有意想不到的影响,这个有时候我们还发现不了.
这就是Redux之所以存在的威力:整个app的state是一个单一的数据结构,state tree.这一点意思是说:在任何时刻,展示给用户的内容仅仅是state tree结果,这就是单一来源的真相(用户界面的显示内容是由state tree来决定的).每一个action接收state,应用相应的修改(例如,添加一个todo),输出更新的state tree.更新的结果渲染展示给用户.里面没有模糊的异步操作,没有变量的引用引起的不经意的修改.这个步骤使得app有了更好的结构,分离关注点,dubugging也更好用了.
Immutable是有Facebook开发的助手函数库,提供一些工具去创建和操作immutable数据结构.尽管在Redux也不是一定要使用它,但是它通过禁止对象的修改,强化了函数式编程方法.有了immutable,当我们想更新一个对象,实际上我们修改的是一个新创建的的对象,原先的对象保持不变.
这里是“Immutable文档”里面的例子:
1 | var map1 = Immutable.Map({a:1, b:2, c:3}); |
我们更新了map1
的一个值,map1
对象保持不变,一个新的对象map3
被创建了.
Immutable在store中被用来储存我们的app的state tree.很快我们会看到Immutable提供了一下操作对象的简单和有效的方法.
配置项目
声明:一些配置有@terops的教程启发.
注意事项:推荐使用Node.js>=4.0.0.你可以使用nvm(node version manager)来切换不同的node.js的版本.
开始配置项目:
1 | mkdir redux-todomvc |
项目的目录结构如下:
1 | ├── dist |
首先创建一个简单的HTML页面,用来运行我们的appdist/index.html
1 |
|
有了这个文件,我们写一个简单的脚本文件看看包安装的情况src/index.js
1 | console.log('Hello world !'); |
我们将会使用[Webpack]来打包成为bundle.js
文件.Webpack的特性是速度,容易配置,大部分是热更新的.代码的更新不需要重新加载,意味着app的state保持热加载.
让我们安装webpack:
npm install —save-dev webpack@1.12.14 webpack-dev-server@1.14.1
app使用ES2015的语法,带来一些优异的特性和一些语法糖.如果你想了解更多的ES2015内容,这个recap是一个不错的资源.
Babel用来把ES2015的语法改变为普通的JS语法:npm install —save-dev babel-core@6.5.2 babel-loader@6.2.4 babel-preset-es2015@6.5.0
我们将使用JSX语法编写React组件,所以让我们安装Babel React package:npm install —save-dev babel-preset-react@6.5.0
配置webpack输出源文件:package.json
1 | "babel": { |
webpack.config.js
1 | module.exports = { |
现在添加React和React热加载组件到项目中:1
2npm install --save react@0.14.7 react-dom@0.14.7
npm install --save-dev react-hot-loader@1.3.0
为了让热加载能运行,webpack.config.js文件中要做一些修改.
webpack.config.js
1 | var webpack = require('webpack'); // Requiring the webpack lib |
配置单元测试框架
我们将使用Mocha和Chai来进行测试工作.这两个工具广泛的被使用,他们的输出内容对于测试驱动开发非常的好.Chai-immutable是一个chai插件,用来处理immutable数据结构.
1 | npm install --save immutable@3.7.6 |
在我们的例子中,我们不会依赖浏览器为基础的测试运行器例如Karma-替代方案是我们使用jsdom库,它将会使用纯javascirpt创建一个假DOM,这样做让我们的测试更加快速.
npm install —save-dev jsdom@8.0.4
我们也需要为测试写一个启动脚本,要考虑到下面的内容.
- 模拟
document
和window
对象,通常是由浏览器提供 - 通过
chia-immutable
告诉chai组件我们要使用immutable数据结构
test/setup.js
1 | import jsdom from 'jsdom'; |
更新一下npm test
脚本package.json
1
2
3
4 "scripts": {
"test": "mocha --compilers js:babel-core/register --require ./test/setup.js 'test/**/*.@(js|jsx)'",
"test:watch": "npm run test -- --watch --watch-extensions jsx"
},
npm run test:watch
命令在windows操作系统下似乎不能工作.
现在,如果我们运行npm run test:watch
,所有test目录里的.js
,.jsx
文件在更新自身或者源文件的时候,将会运行mocha测试.
配置完成了:我们可以在终端中运行webpack-dev-server
,打开另一个终端,npm run test:watch
.在浏览器中打开localhost:8080.检查hello world!
是否出现在终端中.
构建state tree
之前提到过,state tree是能提供app所有信息的数据结构.这个结构需要在实际开发之前经过深思熟虑,因为它将影响一些代码的结构和交互作用.
作为示例,我们app是一个TODO list由几个条目组合而成
每一个条目有一个文本,为了便于操作,设一个id,此外每个item有两个状态之一:活动或者完成:最后一个条目需要一个可编辑的状态(当用户想编辑的文本的时候),
所以我们需要保持下面的数据结构:
也有可能基于他们的状态进行筛选,所以我们天剑filter
条目来获取最终的state tree:
创建UI
首先我们把app分解为下面的组件:
TodoHeader
组件是创建新todo的输入组件TodoList
组件是todo的列表todoItem
是一个todotodoInput
是编辑todo的输入框TodoTools
是显示未完成的条目数量,过滤器和清除完成的按钮footer
是显示信息的,没有具体的逻辑
我们也创建TodoApp
组件组织所有的其他组件.
首次运行我们的组件
提示:运行这个版本
正如我们所见的,我们将会把所有组件放到合并成一个TodoApp
组件.所以让我们添加组件到index.html
文件的#app
DIV中:src/index.jsx
1 | import React from 'react'; |
因为我们在index.jsx
文件中使用JSX语法,需要在wabpack中扩展.jsx
.修改如下:webpack.config.js
1 | entry: [ |
编写todo list UI
现在我们编写第一版本的TodoApp
组件,用来显示todo项目列表:src/components/TodoApp.jsx
1 | import React from 'react'; |
要记住两件事情:
第一个,如果你看到的结果不太好,修复它,我们将会使用todomvc-app-css包来补充一些需要的样式
1 | npm install --save todomvc-app-css@2.0.4 |
我们需要告诉webpack去加载css 样式文件:webpack.config.js
1 | // ... |
然后在inde.jsx
文件中添加样式:src/index.jsx
1 | // ... |
第二件事是:代码似乎很复杂,这就是我们为什么要创建两个或者多个组件的原因:TodoList
和TodoItem
将会分别关注条目列表和单个的条目.
src/components/TodoApp.jsx
1 | import React from 'react'; |
在TodoList
组件中根据获取的props为每一个条目显示一个TodoItem
组件.
src/components/TodoList.jsx
1 | import React from 'react'; |
src/components/TodoItem.jsx
1 | import React from 'react'; |
在我们深入用户的交互操作之前,我们先在组件TodoItem
中添加一个input用于编辑src/componensts/TodoItem.jsx
1 | import React from 'react'; |
TextInput
组件如下src/compoents/TextInput.jsx
1 | import React from 'react'; |
”纯“组件的好处:PureRenderMixin
除了允许函数式编程的样式,我们的UI是单纯的,可以使用PureRenderMixin
来提升速度,正如React 文档:
如果你的React的组件渲染函数是”纯“(换句话就是,如果使用相同的porps和state,总是会渲染出同样的结果),你可以使用mixin在同一个案例转给你来提升性能.
正如React文档(我们也会在第二部分看到TodoApp
组件有额外的角色会阻止PureRenderMixin
的使用)展示的mixin也非常容易的添加到我们的子组件中:npm install --save react-addons-pure-render-mixin@0.14.7
src/components/TodoList.jsc
1 | import React from 'react'; |
src/components/TodoItem/jsx
1 | import React from 'react'; |
src/components/TextInput.jsx
1 | import React from 'react'; |
在list组件中处理用户的actions
好了,现在我们配置好了list组件.然而我们没有考虑添加用户的actions和怎么添加进去组件.
props的力量
在React中,props
对象是当我们实例化一个容器(container)的时候,通过设定的attributes来设定.例如,如果我们实例化一个TodoItem
元素:
1 | <TodoItem text={'Text of the item'} /> |
然后我们在TodoItem
组件中获取this.props.text
变量:
1 | // in TodoItem.jsx |
Redux构架中强化使用props
.基础的原理是state几乎都存在于他的props里面.换一种说法:对于同样一组props,两个元素的实例应该输出完全一样的结果.正如之前我们看到的,整个app的state都包含在一个state tree中:意思是说,state tree 如果通过props
的方式传递到组件,将会完整和可预期的决定app的视觉输出.
TodoList组件
在这一部分和接下来的一部分,我们将会了解一个测试优先的方法.
为了帮助我们测试组件,React库提供了TestUtils
工具插件,有一下方法:
renderIntoDocument
,渲染组件到附加的DOM节点scryRenderDOMComponentsWIthTag
,使用提供的标签(例如li
,input
)在DOM中找到所有的组件实例.scryRederDOMComponentsWithClass
,同上使用的是类Simulate
,模拟用户的actions(例如 点击,按键,文本输入…)
TestUtils
插件没有包含在react
包中,所以需要单独安装npm install --save-dev react-addons-test-utils@0.14.7
我们的第一个测试将确保Todolist
组件中,如果filter
props被设置为active
,将会展示所有的活动条目:
test/components/TodoList_spec.jsx
1 | import React from 'react'; |
我们可以看到测试失败了,期待的是两个活动条目,但是实际上是三个.这是再正常不过的了,因为我们没有编写实际筛选的逻辑:src/components/TodoList.jsx
1 | // ... |
第一个测试通过了.别停下来,让我们添加筛选器:all
和completed:
test/components/TodoList_spec.js
1 | // ... |
第三个测试失败了,因为all
筛选器更新组件的逻辑稍稍有点不同
src/components/TodoList.jsx
1 | // ... |
在这一点上,我们知道显示在app中的条目都是经过filter
属性过滤的.如果在浏览器中看看结果,没有显示任何条目,因为我们还没有设置:src/index.jsx
1 | // ... |
src/components/TodoApp.jsx
1 | // ... |
为了使第二个测试通过,如果条目的状态是complete
我们使用了类complete
,它将会通过props传递向下传递.我们将会使用classnames
包来操作我们的DOM类.
npm install —save classnames
src/components/TodoItem.jsx
1 | import React from 'react'; |
一个item在编辑的时候外观应该看起来不一样,由isEditing
props来包裹.test/components/TodoItem_spec.js
1 | // ... |
为了使测试通过,我们仅仅需要更新itemClass
对象:
src/components/TodoItem.jsx
1 | // ... |
条目左侧的checkbox如果条目完成,应该标记位checked:
test/components/TodoItem_spec.js
1 | // ... |
React有个设定checkbox输入state的方法:defaultChecked
.
src/components/TodoItem.jsx
1 | // ... |
我们也从TodoList
组件向下传递isCompleted
和isEditing
props.
src/components/TodoList.jsx
1 | // ... |
截止目前,我们已经能够在组件中反映出state:例如,完成的条目将会被划线.然而一个webapp将会处理诸如点击按钮的操作.在Redux的模式中,这个操作也通过porps
来执行,稍稍特殊的是通过在props中传递回调函数来完成.通过这种方式,我们再次把UI和App的逻辑处理分离开:组件根本不需要知道按钮点击的操作具体是什么,仅仅是点击触发了一些事情.
为了描述这个原理,我们将会测试如果用户点击了delete按钮(红色X),delteItem
函数将会被调用.
test/components/TodoItem_spec.jsx
1 | / ... |
为了是这个测试通过,我们必须在delete按钮声明一个onClick
句柄,他将会调用经过props传递的deleteItem
函数.
src/components/TodoItem.jsx
1 | // ... |
重要的一点:实际删除的逻辑还没有实施,这个将是Redux的主要作用.
在同一个model,我们可以测试和实施下面的特性:
- 点击checkbox将会调用
toggleComplete
函数 - 双击条目标签,将会调用
editItem
函数
test/components/TodoItem_spec.js
1 | // ... |
src/compoents/TodoItem.jsx
1 | // ... |
我们也从TodoList
组件借助props向下传递editItem
,deleteItem
和toggleComplete
函数.
src/components/TodoList.jsx
1 | // ... |
配置其他组件
现在,你可能对流程有些熟悉了.为了让本文不要太长,我邀请你看看组件的代码,TextInput
(相关提交),TodoHeader
(相关提交),Todotools
和Footer
(相关提交)组件.如果你有任何问题,请留下评论,或着在repo的issue中留下评论.
你可能主要到一些函数,例如editItem
,toggleComplete
诸如此类的,还没有被定义.这些内容将会在教程的下一部分作为Redux actions的组成来定义,所以如果遇到错误,不要担心.
包装起来
在这篇文章中,我已经演示了我的第一个React,Redux和Immutable webapp.我们的UI是模块化的.完全通过测试,准备和实际的app逻辑联系起来.怎么来连接?这些傻瓜组件什么都不知道,怎么让我们可以写出时间旅行的app?